package indv.Cning.cfengsecuritydemo.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import indv.Cning.cfengsecuritydemo.filter.JwtRequestFilter;
import indv.Cning.cfengsecuritydemo.handler.JwtAuthenticationEntryPoint;
import indv.Cning.cfengsecuritydemo.session.ParallelInformationExpiredStrategy;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.oauth2.client.EnableOAuth2Sso;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.session.HttpSessionEventPublisher;

import javax.sql.DataSource;

/**
 * @author Cfeng
 * @date 2022/7/21
 * Spring Security 核心配置类，注入PasswordEncoder对象，加密使用的【Security相比Shiro来说，shiro关于加密的配置更加复杂，手工encode，并且需要配置凭证器，如果使用salt，还要定义】
 * 完成httpsecurity，websecurity，securityManager对象的配置
 * 因为认证逻辑大部分由框架完成，需要将之前定义的Service注入，（Shiro中也可以直接注入Realm）还有ObjectMapper和DataSource对象
 * 不支持继承Adapter，新版直接提供了一个原型HttpSecurity对象，引入即可
 */

//@Configuration    springSecurity的配置类注解使用复合注解
@RequiredArgsConstructor
@EnableWebSecurity //包含配置@Configuration,还有启用authenticationManager
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true,jsr250Enabled = true) //开启注解的使用
//@EnableOAuth2Sso  //开启OAuth2单点登录
public class SecurityConfig {
    //HttpSecurity对象，配置路径相关信息，之前的shiro的factoryBean对象,不需要直接引入，容器中有原型对象
    //security中完成认证和授权的Service【类似realm】，不需要在auth中配置
    private final UserDetailsService userDetailsService;
//    //数据源
    private final DataSource dataSource;
    //jackson工具，对象转为json字符串
    private final ObjectMapper objectMapper;
    //引入jwt的过滤器和异常处理类 =============jwt
//    private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
//
//    private final JwtRequestFilter jwtRequestFilter;

    //使用BcryptPasswordEncoder进行加密和解密
    @Bean
    public PasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    //配置记住我功能的TokenRepository，也是记住我功能数据库模式security自动调用维护的，表结构也是固定的
    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        //这里直接创建实现类，该类会直接获取连接数据库中的persistent_logins表中数据据
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        return tokenRepository;
    }


    //配置路径过滤器链，HttpSecurity
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http
               //前台使用frame标签
               .headers()
               .frameOptions()
               .sameOrigin()
               .and()
               //配置csrf和httpbasic
               .csrf() //关闭网络跨域检查
               .disable()
               .httpBasic()//关闭基础配置
               .disable()
               //配置登录表单
               .formLogin()
               .loginPage("/toLogin/login")//访问登录页
//               .loginPage("/jwtlogin.html")
               .loginProcessingUrl("/authentication/form")  //jwt的登录 /authentication/form 需要和登录表单处理一致，登录访问路径，spring security自动完成
               .defaultSuccessUrl("/main")  //没有具体路径访问，跳转该
               .usernameParameter("userName")  //提交的默认参数
               .passwordParameter("userPwd")
               .permitAll()  //表单相关的路径直接放行
               //配置路径过滤器
               .and()
               .authorizeRequests()
//               .antMatchers("/admin/**").hasRole("admin")   //适合规模小的项目，过多就配置太多
//               .antMatchers("/user/**").hasAuthority("user")
               .anyRequest().authenticated() //登录后都可，不登陆不行
               //配置无权异常处理路径
               .and()
               .exceptionHandling()
               .accessDeniedPage("/403.html")
//               .authenticationEntryPoint(jwtAuthenticationEntryPoint)  //使用的是JWt的异常处理,不用session那一套  ==========
               //配置登出路径
               .and()
               .logout()
               .logoutUrl("/logout")   //配置登出路径，也是security内部完成
               //配置RemeberMe的相关内容，数据库模式的
               .and()
               .rememberMe()
               .rememberMeParameter("rememberMe") //设置表单中登录时checkbox提交的name
               .rememberMeCookieName("rememberMe") //设置访问的cookie的名称
               .tokenValiditySeconds(5 * 60 * 50) //有效期
               .tokenRepository(persistentTokenRepository()) //设置访问的仓库，这里也是security维护
               .userDetailsService(userDetailsService)
               //配置Session，这里使用Spring session避免服务器重启集体下线  内存JVM,最后配置因为配置spring session后下面还是session
               .and()
               .sessionManagement()
               .invalidSessionUrl("/timeout.html") //配置状态过期后的页面
               .maximumSessions(1)  //设置session并发上限为1
               .maxSessionsPreventsLogin(true) //达到上限后阻止登录,false剔除老用户
               .expiredSessionStrategy(new ParallelInformationExpiredStrategy(objectMapper))//失效的处理策略
               .sessionRegistry(sessionRegistry());
//               .sessionCreationPolicy(SessionCreationPolicy.STATELESS)  //使用jwt修改为无状态的session
//               .and()  //将jwt过滤器插入过滤器链的头部,处理所有的jwt请求
//               .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

    /**
     * 放行静态资源，全局配置
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        //方便起见,这里直接将jwtUser的路径放开
        return (web) -> web.ignoring().antMatchers("/js/**", "/img/**","/css/**","/authenticate");
    }

    /**
     * 认证管理器，登录认证相关需要使用，直接注入AuthenticationConfiguration对象（security中提供，和HttpSecurity一样），利用该对象就可以创建
     * 而之前要从数据库中查询对象进行UserService认证鉴权   auth.userDetailsService(userService).passwordEncoder(passwordEncoder)
     * 现在Security会自动注入这两个对象，只需要声明即可
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        //用于访问Session
        return new SessionRegistryImpl();
    }

    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        //必须要告诉Spring信息将Session存储在sessionRegistry
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }
}
